﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Web.Caching;
using log4net;

namespace Sherwood.SignOn.Client.Session
{
    public class SqlSessionRepository : ISessionRepository
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(SqlSessionRepository));

        public SqlSessionRepository(string connectionString, string clientCode, string repositoryIdentifier, int sessionTimeout)
        {
            SsoClientCode = clientCode ?? "";
            ClientIdentifier = !string.IsNullOrEmpty(repositoryIdentifier) ? repositoryIdentifier : SsoClientCode;
            if (string.IsNullOrEmpty(connectionString))
                throw new ArgumentException("Connection string is null or empty!","connectionString");
            ConnectionString = connectionString;
            if (sessionTimeout == 0)
                sessionTimeout = 20;
            SessionTimeoutInMinutes = sessionTimeout;
        }

        public SqlSessionRepository()
        {
            clientSettings settings =
                clientSettings.Load(System.Configuration.ConfigurationManager.AppSettings["ClientConfigurationFile"]);
            ConnectionString = settings.sessionRepository.connectionString;
            SsoClientCode = settings.client.code;
            ClientIdentifier = settings.client.code;
            SessionTimeoutInMinutes = (int)settings.sessionRepository.sessionTimeoutInMinutes;
        }


        #region Configuration

        /// <summary>
        /// SSO client's default unique identifier.
        /// This is used as clientIdentifier if no other identifier is specified.
        /// </summary>
        protected string SsoClientCode { get; set; }

        /// <summary>
        /// Respository database connection string
        /// </summary>
        protected string ConnectionString { get; set; }


        /// <summary>
        /// Respository database connection string
        /// </summary>
        protected int SessionTimeoutInMinutes { get; set; }
        #endregion

        #region ISessionRepository Members

        public string ClientIdentifier { get; set; }

        public string CreateSession()
        {
            //Automatic cleanup of old sessions
            CleanUp();

            //Create new session
            string sessionId = DateTime.Now.ToFileTime() + "-" + Guid.NewGuid().ToString("N").Substring(0, 16);
            string sql = "INSERT INTO SsoClientSessions (sessionid,activated,clientidentifier,datecreated) VALUES (@sessionid,@activated,@clientidentifier,@datecreated)";
            SqlConnection sqlConn = new SqlConnection(ConnectionString);
            SqlCommand sqlCmd = new SqlCommand(sql, sqlConn);
            sqlConn.Open();
            sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
            sqlCmd.Parameters.AddWithValue("@activated", false);
            sqlCmd.Parameters.AddWithValue("@clientidentifier", ClientIdentifier);
            sqlCmd.Parameters.AddWithValue("@datecreated", DateTime.Now);
            sqlCmd.ExecuteNonQuery();
            sqlConn.Close();
            return sessionId;
        }

        public bool ActivateSession(string sessionId)
        {
            //Validate sessionId
            if (!string.IsNullOrEmpty(sessionId))
            {
                string[] parts = sessionId.Split('-');
                if (parts.Length >= 2)
                {
                    string datePart = parts[0];
                    long dateNumber = 0;
                    if (long.TryParse(datePart, out dateNumber))
                    {
                        DateTime sessionDate = DateTime.FromFileTime(dateNumber);

                        //Sessions need to be activated within 5 minutes (arbitrary). 
                        //Usually activation would happen within a matter of seconds.
                        if (Math.Abs((DateTime.Now - sessionDate).TotalMinutes) > 5)
                        {
                            if(Log.IsDebugEnabled)
                                Log.Debug("Session " + sessionId + " is not within activation time frame.");
                            return false;
                        }
                    }
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return false;
            }

            //Check if session exists in database
            string sql = "SELECT activated FROM SsoClientSessions WHERE sessionId = @sessionid";
            SqlConnection sqlConn = new SqlConnection(ConnectionString);
            SqlCommand sqlCmd = new SqlCommand(sql, sqlConn);
            sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
            sqlConn.Open();
            SqlDataReader dr = sqlCmd.ExecuteReader();
            bool activated = false;
            bool sessionExists = false;
            if (dr.Read())
            {
                sessionExists = true;
                activated = dr["activated"] != DBNull.Value ? (bool)dr["activated"] : false;
            }
            dr.Close();
            if (sessionExists && !activated)
            {
                //Activate session
                sql = "UPDATE SsoClientSessions SET dateactivated = @dateactivated, activated = 1, datechecked=@datechecked WHERE sessionid = @sessionid";
                sqlCmd = new SqlCommand(sql, sqlConn);
                sqlCmd.Parameters.AddWithValue("@dateactivated", DateTime.Now);
                sqlCmd.Parameters.AddWithValue("@datechecked", DateTime.Now);
                sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
                sqlCmd.ExecuteNonQuery();
                sqlConn.Close();
                return true;
            }
            sqlConn.Close();
            if(Log.IsDebugEnabled)
                Log.Debug("Session " + sessionId + " does not exist in repository");
            return false;
        }

        public void StoreProfile(string sessionId, string profileXml)
        {
            string sql = "UPDATE SsoClientSessions SET profile = @profile WHERE sessionid = @sessionid";
            SqlConnection sqlConn = new SqlConnection(ConnectionString);
            SqlCommand sqlCmd = new SqlCommand(sql, sqlConn);
            sqlConn.Open();
            sqlCmd = new SqlCommand(sql, sqlConn);
            sqlCmd.Parameters.AddWithValue("@profile", profileXml);
            sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
            sqlCmd.ExecuteNonQuery();
            sqlConn.Close();
        }

        public bool SessionExists(string sessionId)
        {
            string sql = "SELECT count(*) FROM SsoClientSessions WHERE sessionId = @sessionid";
            SqlConnection sqlConn = new SqlConnection(ConnectionString);
            SqlCommand sqlCmd = new SqlCommand(sql, sqlConn);
            sqlConn.Open();
            sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
            int count = Convert.ToInt32(sqlCmd.ExecuteScalar());
            sqlConn.Close();
            return count > 0;
        }

        public bool SessionActive(string sessionId)
        {
            Cache cache = null;
            if (System.Web.HttpContext.Current != null)
                cache = System.Web.HttpContext.Current.Cache;

            if (cache != null)
            {
                //attempt to retrieve cached value
                var cached = cache["SessionIsActive_" + sessionId];
                if (cached is bool)
                    return (bool) cached;
            }

            DateTime timeout = DateTime.Now.AddMinutes(SessionTimeoutInMinutes * -1);

            string sql = "UPDATE SsoClientSessions SET datechecked = @datechecked WHERE sessionId = @sessionid AND activated = 1 AND datechecked > @timeout";
            var sqlConn = new SqlConnection(ConnectionString);
            var sqlCmd = new SqlCommand(sql, sqlConn);
            sqlConn.Open();
            sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
            sqlCmd.Parameters.AddWithValue("@timeout", timeout);
            sqlCmd.Parameters.AddWithValue("@datechecked", DateTime.Now);
            bool hasValidSession = sqlCmd.ExecuteNonQuery() > 0;
            
            if (cache != null)
            {
                if (hasValidSession)
                {
                    //Cache response for 15 seconds to avoid excessive sql server queries
                    const int cacheTimeout = 15;
                    cache.Add("SessionIsActive_" + sessionId, true, null,
                              DateTime.Now.AddSeconds(cacheTimeout),
                              Cache.NoSlidingExpiration,
                              CacheItemPriority.Normal, null);
                }
                else
                    cache.Remove("SessionIsActive_" + sessionId);
            }

            sqlConn.Close();
            return hasValidSession;
        }

        
        public void DeleteSession(string sessionId)
        {
            string sql = "DELETE FROM SsoClientSessions WHERE sessionId = @sessionid";
            SqlConnection sqlConn = new SqlConnection(ConnectionString);
            SqlCommand sqlCmd = new SqlCommand(sql, sqlConn);
            sqlCmd.Parameters.AddWithValue("@sessionid", sessionId);
            sqlConn.Open();
            sqlCmd.ExecuteNonQuery();
            sqlConn.Close();

            if (System.Web.HttpContext.Current != null)
                System.Web.HttpContext.Current.Cache.Remove("SessionIsActive_" + sessionId);
        }

        private static DateTime _lastCleanup;
        public void CleanUp()
        {
            if (_lastCleanup < DateTime.Now.AddMinutes(-60))
            {
                _lastCleanup = DateTime.Now;
                DateTime cleanupDate = DateTime.Now.AddMinutes(SessionTimeoutInMinutes * -1);
                string sql = @"DELETE FROM SsoClientSessions
WHERE (dateactivated IS NULL) AND (datecreated < @cleanupdate) AND (clientidentifier = @clientidentifier) OR
         (dateactivated IS NOT NULL) AND (dateactivated < @cleanupdate) AND (datechecked IS NULL OR
         datechecked < @cleanupdate) AND (clientidentifier = @clientidentifier)";
                SqlConnection sqlConn = new SqlConnection(ConnectionString);
                SqlCommand sqlCmd = new SqlCommand(sql, sqlConn);
                sqlCmd.Parameters.AddWithValue("@cleanupdate", cleanupDate);
                sqlCmd.Parameters.AddWithValue("@clientidentifier", ClientIdentifier);
                sqlConn.Open();
                sqlCmd.ExecuteNonQuery();
                sqlConn.Close();
            }
        }



        #endregion
    }
}
